10.1 将应用部署到独立的服务器
让我们从最简单的部署方法开始——创建一个可执行的二进制文件,并将它放到互联网的某个服务器上运行,这个服务器可以是物理存在的,也可以是由Amazon Web Services(AWS)或者Digital Ocean等供应商创建的虚拟机(VM)。在本节中,我们将要学习如何在运行着Ubuntu Server 14.04系统的服务器上部署Go Web应用。
IaaS、PaaS和SaaS
云计算供应商都会通过不同的模型来为用户提供服务。美国国家标准技术研究所(National Institute of Standards and Technology, US Department of Commerce,NIST)定义了当今广为使用的3种服务模型,分别是基础设施即服务(Infrastructure-as-a-Service,IaaS),平台即服务(Platform-as-a- Service,PaaS)和软件即服务(Software-as-a-Service,SaaS)。 IaaS是这3种模型中最为基本的一种,使用这种模型的供应商将向他们的用户提供包括计算、存储以及网络在内的基本计算能力。提供IaaS服务的例子有AWS的弹性云计算服务(Elastic Cloud Computing Service,EC2)、Google公司的Compute Engine(计算引擎)以及Digital Ocean的Droplets。 使用PaaS模型的供应商会让用户通过他们提供的工具,将应用部署到云端的基础设施之上。提供PaaS服务的例子有Heroku、AWS的Elastic Beanstalk以及Google公司的App Engine。 使用SaaS模型的供应商会向用户提供应用服务。尽管消费者当今使用的绝大多数服务都可以看作是SaaS服务,但是在本书的语境中,我们只会把Heroku的Postgres 数据库服务(Postgres database service,它提供的是基于云的Postgres服务)、AWS的Relational Database Service(关系数据库服务,RDS)以及Google公司的Cloud SQL(云SQL)这样的服务看作是SaaS服务。 在本章中,我们将学习如何利用IaaS和PaaS供应商来部署GoWeb应用。
本书第7章介绍过的简单Web服务由代码清单10-1中的 data.go
和代码清单10-2中的 server.go
这两个文件组成,前者包含了所有指向数据库的连接和所有对数据库进行读写的函数,而后者则包含了 main
函数和Web服务的所有处理逻辑。
代码清单10-1 使用 data.go
访问数据库
package main
import (
"database/sql"
_ "github.com/lib/pq"
)
var Db *sql.DB
func init() {
var err error
Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp
➥sslmode=disable")
if err != nil {
panic(err)
}
}
func retrieve(id int) (post Post, err error) {
post = Post{}
err = Db.QueryRow("select id, content, author from posts where id =
➥$1", id).Scan(&post.Id, &post.Content, &post.Author)
return
}
func (post *Post) create() (err error) {
statement := "insert into posts (content, author) values ($1, $2)
➥returning id"
stmt, err := Db.Prepare(statement)
if err != nil {
return
}
defer stmt.Close()
err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)
return
}
func (post *Post) update() (err error) {
_, err = Db.Exec("update posts set content = $2, author = $3 where id =
➥$1", post.Id, post.Content, post.Author)
return
}
func (post *Post) delete() (err error) {
_, err = Db.Exec("delete from posts where id = $1", post.Id)
return
}
代码清单10-2 定义了Go Web服务的 server.go
package main
import (
"encoding/json"
"net/http"
"path"
"strconv"
)
type Post struct {
Id int `json:"id"`
Content string `json:"content"`
Author string `json:"author"`
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/post/", handleRequest)
server.ListenAndServe()
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
var err error
switch r.Method {
case "GET":
err = handleGet(w, r)
case "POST":
err = handlePost(w, r)
case "PUT":
err = handlePut(w, r)
case "DELETE":
err = handleDelete(w, r)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path))
if err != nil {
return
}
post, err := retrieve(id)
if err != nil {
return
}
output, err := json.MarshalIndent(&post, "", "\t\t")
if err != nil {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(output)
return
}
func handlePost(w http.ResponseWriter, r *http.Request) (err error) {
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
var post Post
json.Unmarshal(body, &post)
err = post.create()
if err != nil {
return
}
w.WriteHeader(200)
return
}
func handlePut(w http.ResponseWriter, r *http.Request) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path))
if err != nil {
return
}
post, err := retrieve(id)
if err != nil {
return
}
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
json.Unmarshal(body, &post)
err = post.update()
if err != nil {
return
}
w.WriteHeader(200)
return
}
func handleDelete(w http.ResponseWriter, r *http.Request) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path))
if err != nil {
return
}
post, err := retrieve(id)
if err != nil {
return
}
err = post.delete()
if err != nil {
return
}
w.WriteHeader(200)
return
}
首先,我们需要使用以下命令编译这段代码:
go build
如果我们把简单Web服务的代码放到一个名为 ws-s
的目录里,那么这个编译命令将产生一个同名的可执行二进制文件。为了部署Web服务 ws-s
,我们需要把ws-s文件复制到服务器里,并将其放置到一个可以通过外部访问的地方。
接着我们只需要登录服务器,并在终端里执行以下命令,就可以运行 ws-s
这个Web服务了:
./ws-s
需要注意的是,因为Web服务现在是在前台运行,所以在服务运行期间,我们将无法执行其他操作。与此同时,我们也无法简单地通过 &
命令或者 bg
命令在后台运行这个服务,因为这样做的话,一旦用户登出,Web服务就会被杀死。
避免上述问题的一种方法就是使用 nohup
命令,让操作系统在用户注销时,把发送至Web服务的 HUP
(hangup,挂起)信号忽略掉:
nohup ./ws-s &
执行上述命令将导致Web服务被放到后台运行,并且不用担心因为 HUP
信号而被杀死。以这种方式启动的Web服务仍会如常地与客户端进行连接,但现在的Web服务将忽略所有挂起或者退出信号。因为这种状态下运行的Web服务在崩溃时将不会有任何提醒,所以在服务崩溃或者服务器重启之后,用户必须重新登入系统并重启服务。
除 nohup
之外,持续运行Web服务的另一种方法是使用Upstart或者systemd这样的 init
守护进程: init
进程是类Unix系统在启动时运行的第一个进程,该进程由内核负责启动,它会一直运行直到系统关闭为止,并且它还是其他所有进程直接或间接的祖先。
Upstart是由Ubuntu创建的一个基于事件的 init
替代品,尽管现在systemd也越来越受到大家的青睐,但考虑到这两个工具都能够完成本节介绍的工作,并且Upstart的使用方法相对来说要更为简单一些,所以我们接下来将要学习如何使用Upstart来持续地运行Web服务。
为了使用Upstart,用户首先需要创建一个对应的Upstart任务配置文件,并将该文件放到 etc/init
目录里面。对简单Web服务来说,我们将创建代码清单10-3所示的 ws.conf
文件,并将它放到 etc/init
目录里面。
代码清单10-3 简单Web服务的Upstart任务配置文件
respawn
respawn limit 10 5
setuid sausheong
setgid sausheong
exec /go/src/github.com/sausheong/ws-s/ws-s
这个Upstart任务配置文件非常简单和直接。文件中的每个Upstart任务都由一个或任意多个称为节(stanzas)的命令块组成。第一节 respawn
指示当任务失效(fail)时,Upstart将对其实施重新派生(respawn)或者重新启动。第二节 respawn limit 10 5
为 respawn
设置了参数,它指示Upstart最多只会尝试重新派生该任务10次,并且每次尝试之间会有5 s的间隔;在用完了10次重新派生的机会之后,Upstart将不再尝试重新派生该任务,并将该任务视为已失效。第三节和第四节负责设置运行进程的用户以及用户组,而最后一节则是Upstart在启动任务时需要运行的可执行文件。
为了启动上述Upstart任务,我们需要在终端里面执行以下命令:
sudo start ws
ws start/running, process 2011
这个命令将触发Upstart读取 /etc/init/ws.conf
任务配置文件并启动任务。本节以管中窥豹的方式,快速地了解了如何使用简单的Upstart任务运行一个Go Web应用,但是除这里介绍的内容之外,Upstart的任务配置文件还有其他不同的节可供使用,并且Upstart的任务也拥有多种不同的配置方式可以使用,不过这些内容不在本书的介绍范围之内,有兴趣的读者可以自行通过互联网进行了解。
为了验证Upstart是否能够正确地运行和管理 ws-s
服务,我们可以尝试在Upstart任务启动之后,杀死正在运行的 ws-s
服务:
ps -ef | grep ws
sausheo+ 2011 1 0 17:23 ? 00:00:00 /go/src/github.com/sausheong/ws-s/ws-s
sudo kill -0 2011
ps -ef | grep ws
sausheo+ 2030 1 0 17:23 ? 00:00:00 /go/src/github.com/sausheong/ws-s/ws-s
注意看,在 kill
命令执行之前, ws-s
进程的ID为 2011
,但是在 kill
命令执行之后, ws-s
进程的ID变成了 2030
——这是因为Upstart在 kill
命令执行之后,察觉到了 ws-s
进程已被关闭,于是它重启了 ws-s
进程,从而导致 ws-s
进程的ID发生了变化。
最后,因为大部分Web应用都部署在标准HTTP端口(即80端口)之上,所以读者在实际部署时,应该将简单Web服务代码中的端口号从现在的8080改为80,或者通过某种机制将8080端口的流量代理或者重定向到80端口。